﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using XcoAppSpaces.Core.Ports.Remoting;
using XcoAppSpaces.Core.Ports.Utils;

namespace XcoAppSpaces.Core.Ports
{
	/// <summary>
	/// Non generic portset variant, which allows to define a larger number of ports, that is not limited 
	/// to the number of generic arguments. Should only be used if a portset many ports is required - otherwise
	/// prefer to use the generic portset variant.
	/// </summary>
	public class PortSet : IPortSet, IPort, IRemotablePort, IPortBypass, IRemotedPost
	{
		/// <summary>
		/// List of the portset's ports.
		/// </summary>
		protected readonly Dictionary<Type, IPort> _ports;

		/// <summary>
		/// Creates a new portset for the given message types.
		/// </summary>
		/// <param name="types">The message types to use.</param>
		public PortSet(params Type[] types)
			: this(types.Select(t => (IPort)Activator.CreateInstance(typeof (Port<>).MakeGenericType(t))).ToArray())
		{
		}

		/// <summary>
		/// Creates a new portset using the given ports.
		/// </summary>
		/// <param name="ports">The ports to use.</param>
		public PortSet(params IPort[] ports)
		{
			_ports = ports.ToDictionary(p => p.MessageType, p => p);
		}

		/// <summary>
		/// Returns an enumerable containing all ports of this portset.
		/// </summary>
		public IEnumerable<IPort> Ports { get { return _ports.Values; } }

		/// <summary>
		/// Implemented from interface IPort. Since a portset doesn't have a single message type, this property throws an exception.
		/// </summary>
		public Type MessageType { get { throw new NotSupportedException("A portset does not have a single message type."); } }

		/// <summary>
		/// Posts an item to the portset. If the message doesn't correspond to any the  portset's ports, an exception is thrown.
		/// </summary>
		/// <param name="item">The item to be posted.</param>
		public void PostUnknownType(object item)
		{
			PostUnknownType(item, null, null);
		}

		/// <summary>
		/// Posts an item to the portset. If the message doesn't correspond to any the  portset's ports, an exception is thrown.
		/// The given error port is used as causality, when exceptions are thrown.
		/// </summary>
		/// <param name="item">The item to be posted.</param>
		/// <param name="errorPort">The error port to be used when exceptions are thrown.</param>
		public void PostUnknownType(object item, IPort<Exception> errorPort)
		{
			PostUnknownType(item, errorPort, null);
		}

		/// <summary>
		/// Posts an item to the portset. If the message doesn't correspond to any the  portset's ports, an exception is thrown.
		/// The given error and ack ports are used as causality.
		/// </summary>
		/// <param name="item">The item to be posted.</param>
		/// <param name="errorPort">The error port to be used when exceptions are thrown.</param>
		/// <param name="ackPort">The port where an ack message should be posted.</param>
		public void PostUnknownType(object item, IPort<Exception> errorPort, IPort<Ack> ackPort)
		{
			var port = FindPortForMessageType(item.GetType());
			port.PostUnknownType(item, errorPort, ackPort);
		}

		/// <summary>
		/// Posts an item to the port and returns a task that completes as soon as the item was sent (or goes
		/// into faulted state when sending fails). If the port is not remote, then a task will be returned
		/// that is already completed. If the message doesn't correspond to the port's type, an exception is thrown.
		/// </summary>
		/// <param name="item">The item to be posted.</param>
		/// <returns>A task to wait for the item to be sent.</returns>
		public Task PostUnknownTypeAndAwaitSent(object item)
		{
			return PostUnknownTypeAndAwaitSent(item, null);
		}

		/// <summary>
		/// Posts an item to the port and returns a task that completes as soon as the item was sent (or goes
		/// into faulted state when sending fails). If the port is not remote, then a task will be returned
		/// that is already completed. If the message doesn't correspond to the port's type, an exception is thrown.
		/// Since communication errors are returned to the task, the given error port does not react to
		/// communication errors, but only to errors happening later when the message is being processed.
		/// </summary>
		/// <param name="item">The item to be posted.</param>
		/// <param name="errorPort">Error port for receiving exceptions that happen during message processing.</param>
		/// <returns>A task to wait for the item to be sent.</returns>
		public Task PostUnknownTypeAndAwaitSent(object item, IPort<Exception> errorPort)
		{
			var port = FindPortForMessageType(item.GetType());
			return port.PostUnknownTypeAndAwaitSent(item, errorPort);
		}

		/// <summary>
		/// Searches for a port to which the given message type can be posted. No only finds exact matches,
		/// but also anything that is assignable (e.g. a string message would be assignable to a port of type object).
		/// If no port is found, an exception is thrown.
		/// </summary>
		/// <param name="msgType">The message type for which a port is required.</param>
		/// <returns>The port that was found.</returns>
		private IPort FindPortForMessageType(Type msgType)
		{
			//1. simplest possibility: exact match
			IPort port;
			if (_ports.TryGetValue(msgType, out port))
				return port;
			//2. search by looking if the type is assignable to any of the port message types
			foreach (var portDef in _ports)
			{
				var typeInfo = msgType.GetTypeInfo();
				if (portDef.Key.GetTypeInfo().IsAssignableFrom(typeInfo))
					return portDef.Value;
			}
			throw new ArgumentException("This portset does not contain a port of type " + msgType + ".");
		}

		/// <summary>
		/// Registers a receiver which should be used for processing items that are posted to any of the portset's ports.
		/// An exception is thrown if the delegate does not take a parameter of type object.
		/// </summary>
		/// <param name="method">The receiver delegate.</param>
		public void RegisterUnknownTypeReceiver(Delegate method)
		{
			RegisterUnknownTypeReceiver(method, null);
		}

		/// <summary>
		/// Registers a receiver which should be used for processing items that are posted to any of the portset's ports.
		/// An exception is thrown if the delegate does not take a parameter of type object.
		/// </summary>
		/// <param name="method">The receiver delegate.</param>
		/// <param name="options">Processing options, like MaxDegreeOfParallelism.</param>
		public void RegisterUnknownTypeReceiver(Delegate method, PortReceiverOptions options)
		{
			if (method is Action<object>)
				RegisterUnknownTypeReceiver((Action<object>)method, options);
			else if (method is Func<object, Task>)
				RegisterUnknownTypeReceiver((Func<object, Task>)method, options);
			else
				throw new ArgumentException("Cannot register delegate " + method + " at a portset - only delegates allowed that take messages of type object.");
		}

		private void RegisterUnknownTypeReceiver(Action<object> receiver, PortReceiverOptions options)
		{
			foreach (var port in _ports.Values)
			{
				port.RegisterUnknownTypeReceiver(receiver, options);
			}
		}

		private void RegisterUnknownTypeReceiver(Func<object, Task> receiver, PortReceiverOptions options)
		{
			foreach (var port in _ports.Values)
			{
				port.RegisterUnknownTypeReceiver(receiver, options);
			}
		}

		/// <summary>
		/// Unregisters the receiver from the portset.
		/// </summary>
		public void UnregisterReceiver()
		{
			foreach (var port in _ports.Values)
			{
				port.UnregisterReceiver();
			}
		}

		/// <summary>
		/// This field can be set to mark the portset as a remote port. It gives access to remote port related infos
		/// like the remote address.
		/// </summary>
		public XcoPortInfo RemotePortInfo 
		{ 
			get { return ((IRemotablePort) Ports.First()).RemotePortInfo; }
			set
			{
				foreach (var port in _ports.Values)
				{
					((IRemotablePort) port).RemotePortInfo = value;
				}
			}
		}

		/// <summary>
		/// Registers a receiver that bypasses most of the normal port logic (like exception handling), and allows direct access to the PortElement instance.
		/// </summary>
		/// <param name="bypassAction">The action to be called.</param>
		/// <param name="options">Receiver options.</param>
		void IPortBypass.RegisterBypassReceiver(Action<IPort, IPortElement> bypassAction, PortReceiverOptions options)
		{
			foreach (var port in _ports.Values)
			{
				((IPortBypass)port).RegisterBypassReceiver(bypassAction, options);
			}
		}

		void IRemotedPost.PostRemoted(object item, IEnumerable<ICausality> causalities, MessageContext msgContext)
		{
			var port = FindPortForMessageType(item.GetType());
			((IRemotedPost)port).PostRemoted(item, causalities, msgContext);
		}
	}
}
